package org.krayne.swivel.system;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;

/**
 * A {@code MemUsageBar} is a progress bar that displays the amount of
 * memory being used by the current application.
 * 
 * IMPORTANT: When one is instantiated, a worker thread is spawned that updates
 * the mem usage at a specifiable interval. (This SOUNDS bad, but notice that
 * this class if {@code final}.) Because an updater thread is created, it is
 * up to the developer to stop that thread by invoking {@code stopUpdating()}.
 * 
 * @author dhsu
 */
public final class MemUsageBar extends JPanel {
    private static final long serialVersionUID = 6415510167255483556L;
    
    // defaults
    private static final long DEFAULT_UPDATE_INTERVAL_MILLIS = 1500;
    private static final int DEFAULT_PADDING = 2;
    
    // the actual progress bar
    private final JProgressBar progressBar;
    
    // update thread-related
    private volatile long updateIntervalMillis;
    private final MemUsageUpdater updater;
    
    /**
     * Constructs a mem usage bar with a predefined default update interval.
     */
    public MemUsageBar() {
        this(DEFAULT_UPDATE_INTERVAL_MILLIS);
    }
    
    /**
     * Constructs a mem usage bar with the specified update interval.
     * 
     * @param updateIntervalMillis the update interval in milliseconds
     */
    public MemUsageBar(long updateIntervalMillis) {
        super(new BorderLayout());
        this.updateIntervalMillis = updateIntervalMillis;
        this.progressBar = new JProgressBar(0, 100);
        this.progressBar.setStringPainted(true);
        
        // layout
        this.add(progressBar, BorderLayout.CENTER);
        this.setBorder(BorderFactory.createEmptyBorder(DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING));
        
        // run update thread
        this.updater = new MemUsageUpdater();
        this.updater.execute();
    }
    
    /**
     * Sets the update interval.
     * 
     * @param updateIntervalMillis the update interval in milliseconds
     */
    public void setUpdateIntervalMillis(long updateIntervalMillis) {
        if(updateIntervalMillis <= 0) {
            return;
        }
        
        this.updateIntervalMillis = updateIntervalMillis;
    }
    
    /**
     * Kills the update thread. The progress bar will no longer be updated
     * after this method is called. You should call this method for a proper
     * cleanup.
     */
    public void stopUpdating() {
        updater.cancel(true);
    }
    
    /**
     * {@inheritDoc}
     */
    public void setFont(Font font) {
        super.setFont(font);
        if(progressBar != null) {
            progressBar.setFont(font);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void setForeground(Color fg) {
        super.setForeground(fg);
        if(progressBar != null) {
            progressBar.setForeground(fg);
        }
    }

    //--------------------------------------------------------------------------
    
    /**
     * An immutable class that contains information about memory usage.
     */
    private static class MemUsage {
        private final double usedMemMegs;
        private final double totalMemMegs;
        private final String memUsageString;
        private final double percentMemUsed;
        private static final NumberFormat memNumberFormat;
        static {
            memNumberFormat = new DecimalFormat();
            memNumberFormat.setMaximumFractionDigits(1);
            memNumberFormat.setMinimumFractionDigits(1);
        }
        
        /**
         * Constructs a mem usage object with the specified values.
         * 
         * @param usedMemMegs the number of megabytes of memory used
         * @param totalMemMegs the total number of megabytes of memory
         */
        public MemUsage(double usedMemMegs, double totalMemMegs) {
            // memoize everything on construction
            this.usedMemMegs = usedMemMegs;
            this.totalMemMegs = totalMemMegs;
            this.percentMemUsed = (usedMemMegs / totalMemMegs) * 100.0;
            this.memUsageString = memNumberFormat.format(usedMemMegs) + " / " +
                                  memNumberFormat.format(totalMemMegs) + " MB";
        }
        
        /**
         * Gets the total memory in megabytes.
         * 
         * @return the total memory in megabytes
         */
        public double getTotalMemMegs() {
            return this.totalMemMegs;
        }
        
        /**
         * Gets the used memory in megabytes.
         * 
         * @return the used memory in megabytes
         */
        public double getUsedMemMegs() {
            return this.usedMemMegs;
        }
        
        /**
         * Gets the percent of memory used.
         * 
         * @return the percent of memory used
         */
        public double getPercentMemUsed() {
            return this.percentMemUsed;
        }
        
        /**
         * {@inheritDoc}
         */
        public String toString() {
            return this.memUsageString;
        }
    }
    
    //--------------------------------------------------------------------------
    
    /**
     * A {@code SwingWorker} that updates the mem usage progress bar.
     */
    private class MemUsageUpdater extends SwingWorker<Void, MemUsage> {
        private static final double BYTES_PER_MEG = 1024.0 * 1024.0;
        
        /**
         * Computes the used and total memory every interval until canceled.
         * 
         * {@inheritDoc}
         */
        protected Void doInBackground() throws Exception {
            // loop forever until the thread is interrupted, probably by
            // a cancel request
            while(!Thread.currentThread().isInterrupted()) {
                double totalMemMegs = Runtime.getRuntime().totalMemory() / BYTES_PER_MEG;
                double freeMemMegs = Runtime.getRuntime().freeMemory() / BYTES_PER_MEG;
                double usedMemMegs = totalMemMegs - freeMemMegs;
                this.publish(new MemUsage(usedMemMegs, totalMemMegs));
                
                // sleep for the update interval
                try {
                    Thread.sleep(updateIntervalMillis);
                } catch(InterruptedException e) {
                    break;
                }
            }

            return null;
        }

        /**
         * {@inheritDoc}
         */
        protected void process(List<MemUsage> memUsages) {
            if(memUsages.isEmpty()) {
                return;
            }
            
            MemUsage lastMemUsage = memUsages.get(memUsages.size() - 1);
            progressBar.setString(lastMemUsage.toString());
            progressBar.setValue((int) lastMemUsage.getPercentMemUsed());
        }
    }
}
